/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.help;

import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.resources.FileResource;

import ws.quokka.core.bootstrap_util.Assert;
import ws.quokka.core.bootstrap_util.IOUtils;
import ws.quokka.core.bootstrap_util.ProjectLogger;
import ws.quokka.core.bootstrap_util.PropertiesUtil;
import ws.quokka.core.bootstrap_util.VoidExceptionHandler;
import ws.quokka.core.metadata.Metadata;
import ws.quokka.core.metadata.MetadataAware;
import ws.quokka.core.model.Dependency;
import ws.quokka.core.model.DependencySet;
import ws.quokka.core.model.ModelFactory;
import ws.quokka.core.model.ModelFactoryAware;
import ws.quokka.core.model.Path;
import ws.quokka.core.model.PathGroup;
import ws.quokka.core.model.Plugin;
import ws.quokka.core.model.PluginDependency;
import ws.quokka.core.model.PluginDependencyTarget;
import ws.quokka.core.model.Project;
import ws.quokka.core.model.ProjectModel;
import ws.quokka.core.model.Target;
import ws.quokka.core.plugin_spi.support.ResourcesList;
import ws.quokka.core.repo_spi.RepoArtifact;
import ws.quokka.core.repo_spi.RepoArtifactId;
import ws.quokka.core.repo_spi.RepoPath;
import ws.quokka.core.repo_spi.RepoPathSpec;
import ws.quokka.core.repo_spi.Repository;
import ws.quokka.core.repo_spi.RepositoryAware;
import ws.quokka.core.util.AnnotatedProperties;
import ws.quokka.core.util.Annotations;
import ws.quokka.core.util.Strings;
import ws.quokka.core.util.URLs;
import ws.quokka.core.util.xml.Document;
import ws.quokka.core.util.xml.Element;

import ws.quokka.plugin.devreport.DevReportHelper;
import ws.quokka.plugin.devreport.Report;
import ws.quokka.plugin.lifecycle.LifeCycleImpl;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.net.URL;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;


/**
 *
 */
public class HelpPlugin extends LifeCycleImpl implements MetadataAware, RepositoryAware, ModelFactoryAware {
    //~ Instance fields ------------------------------------------------------------------------------------------------

    private ModelFactory modelFactory;
    private int annotationId;
    private File destDir;
    private DevReportHelper helper;
    private Map actualProperties;
    private AnnotatedProperties annotatedProperties;
    private Element annotationsRoot;
    private Metadata metadata;
    private Repository repository;

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void setModelFactory(ModelFactory modelFactory) {
        this.modelFactory = modelFactory;
    }

    public void initialise() {
        super.initialise();
        helper = new DevReportHelper(getResources(), getProperties(), logger());
    }

    public void collect() {
        helper.addResource(new FileResource(getProject(), getProject().getProperty("quokka.project.file")));

        if (getProperties().getBoolean("propogate")) {
            propogate();
        }
    }

    public void propogate() {
        helper.propogateResources();
    }

    public void report() throws IOException {
        Report report = helper.createReport();
        report.setIndexFile(generateReport(((FileResource)helper.getLocalResources().iterator().next()).getFile())
            .getIndexFile());
        helper.addReport(report);
    }

    public void aggregateReport() throws IOException {
        helper.addReport();
        report(helper.getAggregatedResources());
    }

    private void report(ResourcesList resourcesList) throws IOException {
        List reports = new ArrayList();

        for (Iterator i = resourcesList.iterator(); i.hasNext();) {
            File projectFile = ((FileResource)i.next()).getFile();
            reports.add(generateReport(projectFile));
        }

        // TODO: Summary across projects
        File aggregateDir = getProperties().getFile("destDir");

        // Copy resources (images, css, etc)
        Copy copy = (Copy)getUtils().init(getProject().createTask("copy"), "copy");
        copy.setTodir(aggregateDir);

        FileSet set = getProperties().getFileSet("aggResources");
        copy.addFileset(set);
        copy.perform();

        Document document = Document.parse(getResources().getLocalResources().getAsStream("aggregate-index.html"));
        Element tableEl = document.getRoot().getChild("table");

        for (Iterator i = reports.iterator(); i.hasNext();) {
            Report report = (Report)i.next();
            Element trEl = tableEl.addChild("tr");
            trEl.addChild("td").addChild("a").setAttribute("class", "name")
            .setAttribute("href", helper.getRelativePath(report, aggregateDir)).addText(report.getName());
        }

        File index = new File(aggregateDir, "index.html");
        save(index, document);
    }

    private Report generateReport(File projectFile) throws IOException {
        ProjectModel defaultModel = modelFactory.getProjectModel(projectFile, Collections.EMPTY_LIST, true, null,
                new ProjectLogger(getResources().getProject()), null);
        Project project = defaultModel.getProject();

        destDir = new File(getProperties().getFile("destDir"), project.getName());

        Document document = generateReport(defaultModel);
        File index = new File(destDir, "report.html");
        save(index, document);

        return new Report(project.getName(), null, index);
    }

    public void help() throws IOException {
        // Get the model for the current project instance
        ProjectModel model = (ProjectModel)getProject().getReference("quokka.projectModel");
        destDir = getProperties().getFile("tempDir");
        actualProperties = PropertiesUtil.getProperties(getProject()); // TODO: remove this, make model evaluate properly

        Document document = generateReport(model);
        File help = new File(destDir, "help.html");
        save(help, document);
        launch(help);
    }

    private void launch(File file) {
        if (getProperties().getBoolean("launch", true)) {
            URLs.openBrowser(URLs.toURL(file), getProject().getProperty("quokka.global.browser"));
        }
    }

    private void copyReportResources() {
        // Copy resources (images, css, etc)
        Copy copy = (Copy)getUtils().init(getProject().createTask("copy"), "copy");
        copy.setTodir(destDir);

        FileSet set = getProperties().getFileSet("resources");
        copy.addFileset(set);
        copy.perform();
    }

    private Element addHeader(Document document, String title, String pathToRoot) {
        Element rootEl = document.addRootElement("html");
        Element headEl = rootEl.addChild("head");
        headEl.addChild("link").setAttribute("rel", "stylesheet").setAttribute("type", "text/css").setAttribute("href",
            pathToRoot + "css/styles.css");
        headEl.addChild("script").setAttribute("language", "JavaScript").setAttribute("type", "text/javascript")
        .setAttribute("src", "scripts/scripts.js").addChild("span");
        headEl.addChild("title").addText(title);

        return rootEl;
    }

    private Element addSection(Element bodyEl, String title, String id, boolean expanded) {
        Element h2El = bodyEl.addChild("h2");
        h2El.addText(title + " ");
        h2El.addChild("a").setAttribute("class", "morelink").setAttribute("id", "x" + id)
        .setAttribute("href", "javascript:viewMore('" + id + "');").addText(expanded ? "Hide..." : "Show...");

        return bodyEl.addChild("div").setAttribute("id", id).setAttribute("style",
            expanded ? "display:block" : "display:none");
    }

    private Document generateReport(ProjectModel model)
            throws IOException {
        getUtils().deleteFile(destDir);
        getUtils().mkdir(destDir);
        copyReportResources();

        // Create the report (xhtml)
        String title = model.getProject().getName() + " Help";

        Document document = Document.create();
        Element rootEl = addHeader(document, title, "");

        Element bodyEl = rootEl.addChild("body");
        bodyEl.addChild("script").setAttribute("language", "JavaScript").setAttribute("type", "text/javascript")
        .setAttribute("src", "scripts/wz_tooltip.js").addChild("span");
        bodyEl.addChild("script").addText("window.tt_HideSrcTags.done=true;");

        bodyEl.addChild("h1").addText("Project: " + model.getProject().getName());
        bodyEl.addChild("p").addText((model.getProject().getDescription() == null) ? ""
                                                                                   : model.getProject().getDescription());

        annotationsRoot = new Element(bodyEl.getElement().getOwnerDocument().createElement("div"));

        //        annotationsRoot = bodyEl.addChild("div");
        Element pEl = bodyEl.addChild("p");
        pEl.addChild("b").addText("Active profiles: ");
        pEl.addText(Strings.join(model.getProject().getActiveProfiles().getElements().iterator(), ", "));

        Element divEl = addSection(bodyEl, "Major Targets", "major-targets", true);
        generateTargetsSummary(model, divEl, true);

        divEl = addSection(bodyEl, "Minor Targets", "minor-targets", false);
        generateTargetsSummary(model, divEl, false);

        divEl = addSection(bodyEl, "Project Paths", "paths", true);
        generateProjectPaths(model, divEl);

        divEl = addSection(bodyEl, "Plugin Paths", "pluginPaths", false);
        generatePluginPaths(model, divEl);

        divEl = addSection(bodyEl, "Dependencies Sets", "depsets", true);

        //        divEl.addChild("span");
        generateDependencySets(model, divEl);

        Element sysPropsEl = addSection(bodyEl, "System properties", "sysprops", false);

        Element globalPropsEl = addSection(bodyEl, "Global properties", "globalprops", true);

        Element pluginPropsEl = addSection(bodyEl, "Plugin properties for abstract targets", "pluginprops", true);

        divEl = addSection(bodyEl, "Targets in Detail", "targets", true);
        generateTargets(model, divEl);

        generateSystemProperties(sysPropsEl);
        generatePluginProperties(model, pluginPropsEl);
        generateGlobalProperties(globalPropsEl);
        bodyEl.getElement().appendChild(annotationsRoot.getElement());

        return document;
    }

    private void generateDependencySets(ProjectModel model, Element divEl) {
        generateDependencySet(model.getProject().getDependencySet(), divEl);
    }

    public void generateDependencySet(DependencySet set, Element divEl) {
        Map plugins = new TreeMap();
        Map dependencies = new TreeMap();
        Map subsets = new TreeMap();

        for (Iterator i = set.getDependencies().iterator(); i.hasNext();) {
            Dependency dependency = (Dependency)i.next();

            if (dependency instanceof PluginDependency) {
                plugins.put(dependency.getId().toShortString(), dependency);
            } else {
                dependencies.put(dependency.getId().toShortString(), dependency);
            }
        }

        for (Iterator i = set.getSubsets().iterator(); i.hasNext();) {
            DependencySet subSet = (DependencySet)i.next();
            subsets.put(subSet.getArtifact().getId().toShortString(), subSet);
        }

        if (set.getArtifact() == null) {
            // Root project
            divEl.addChild("p").addChild("b").addText("Project");
        } else {
            //            divEl = divEl.addChild("div").setAttribute("class", "indent");
            addId(divEl.addChild("p"), set.getArtifact().getId(), false, true);
        }

        divEl = divEl.addChild("div").setAttribute("class", "indent");

        if (dependencies.size() > 0) {
            divEl.addChild("b").addText("Dependencies");

            Element tableEl = divEl.addChild("div").setAttribute("class", "indent").addChild("table");

            for (Iterator i = dependencies.values().iterator(); i.hasNext();) {
                Dependency dependency = (Dependency)i.next();
                Element trEl = tableEl.addChild("tr");
                addId(trEl.addChild("td"), dependency.getId(), false, false);

                if (dependency.getPathSpecs().size() > 0) {
                    //                    Element pathSpecsEl = el.addChild("div").setAttribute("class", "indent");
                    Element tdEl = trEl.addChild("td");

                    for (Iterator j = dependency.getPathSpecs().iterator(); j.hasNext();) {
                        RepoPathSpec pathSpec = (RepoPathSpec)j.next();
                        tdEl.addText(pathSpec.toShortString());

                        if (j.hasNext()) {
                            tdEl.addText(", ");
                        }
                    }
                }
            }
        }

        if (plugins.size() > 0) {
            divEl.addChild("b").addText("Plugins");

            Element tableEl = divEl.addChild("div").setAttribute("class", "indent").addChild("table");

            //            Element el = divEl.addChild("div").setAttribute("class", "indent");
            for (Iterator i = plugins.values().iterator(); i.hasNext();) {
                PluginDependency plugin = (PluginDependency)i.next();
                Element trEl = tableEl.addChild("tr");
                addId(trEl.addChild("td"), plugin.getId(), false, false);

                for (Iterator j = plugin.getTargets().iterator(); j.hasNext();) {
                    PluginDependencyTarget target = (PluginDependencyTarget)j.next();
                    Element tdEl = trEl.addChild("td");
                    tdEl.addText(target.toShortString());

                    if (j.hasNext()) {
                        tdEl.addText(", ");
                    }
                }
            }
        }

        if (subsets.size() > 0) {
            divEl.addChild("b").addText("Subsets");

            Element el = divEl.addChild("div").setAttribute("class", "indent");

            for (Iterator i = subsets.values().iterator(); i.hasNext();) {
                DependencySet subSet = (DependencySet)i.next();
                generateDependencySet(subSet, el);
            }
        }
    }

    private void generatePluginProperties(ProjectModel model, Element pluginPropsEl) {
        Map plugins = new TreeMap();
        Map targets = model.getTargets();

        for (Iterator i = targets.values().iterator(); i.hasNext();) {
            Target target = (Target)i.next();
            plugins.put(target.getPlugin().getNameSpace(), target.getPlugin());
        }

        for (Iterator i = plugins.values().iterator(); i.hasNext();) {
            Plugin plugin = (Plugin)i.next();
            boolean first = true;
            Element tableEl = null;

            for (Iterator j = actualProperties.entrySet().iterator(); j.hasNext();) {
                Map.Entry entry = (Map.Entry)j.next();
                String key = (String)entry.getKey();
                String value = (String)entry.getValue();

                if (key.startsWith(plugin.getNameSpace() + ".")) {
                    if (first) {
                        first = false;

                        Element pEl = pluginPropsEl.addChild("p").setAttribute("style", "margin-top:10px");
                        pEl.addChild("b").addText(plugin.getArtifact().getId().toShortString());
                        pEl.addText(" (Name space: " + plugin.getNameSpace() + ")");
                        tableEl = pluginPropsEl.addChild("div").setAttribute("class", "indent").addChild("table");
                    }

                    Element trEl = tableEl.addChild("tr");
                    addTooltip(trEl.addChild("td").addChild("b").addText(key.substring(plugin.getNameSpace().length()
                                + 1)), annotatedProperties.getAnnotation(key));
                    trEl.addChild("td").addText(value);
                    j.remove();
                }
            }
        }
    }

    private void generateSystemProperties(Element sysPropsEl) {
        Element tableEl = sysPropsEl.addChild("table");

        for (Iterator i = System.getProperties().entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String key = (String)entry.getKey();
            String value = (String)actualProperties.remove(key);

            if (value == null) {
                value = (String)entry.getValue();
            }

            Element trEl = tableEl.addChild("tr");
            addTooltip(trEl.addChild("td").addChild("b").addText(key), annotatedProperties.getAnnotation(key));
            trEl.addChild("td").addText(value);
        }
    }

    private void generateGlobalProperties(Element globalEl) {
        Element tableEl = globalEl.addChild("table");

        for (Iterator i = actualProperties.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String key = (String)entry.getKey();
            String value = (String)entry.getValue();
            Element trEl = tableEl.addChild("tr");
            addTooltip(trEl.addChild("td").addChild("b").addText(key), annotatedProperties.getAnnotation(key));
            trEl.addChild("td").addText(value);
        }
    }

    private void generateTargetsSummary(ProjectModel model, Element divEl, boolean main) {
        Element tableEl = divEl.addChild("table");
        Map targets = new TreeMap(model.getTargets());

        for (Iterator i = targets.values().iterator(); i.hasNext();) {
            Target target = (Target)i.next();

            if (target.isMain() == main) {
                Element trEl = tableEl.addChild("tr");
                trEl.addChild("td").addChild("a").setAttribute("href", "#" + target.getName()).addChild("b").addText((
                        target.getAlias() == null
                    ) ? target.getName() : target.getAlias());
                trEl.addChild("td").addText((target.getDescription() == null) ? "" : target.getDescription());
            }
        }
    }

    private void generateTargets(ProjectModel model, Element divEl) {
        Map targets = new TreeMap(model.getTargets());
        annotatedProperties = model.getProperties();
        actualProperties = new TreeMap((actualProperties == null) ? annotatedProperties : actualProperties);

        for (Iterator i = targets.values().iterator(); i.hasNext();) {
            Target target = (Target)i.next();
            divEl.addChild("a").setAttribute("name", target.getName()).addChild("span");

            Element pEl = divEl.addChild("p");
            pEl.addChild("span").setAttribute("class", "targetId").addText(target.getName());
            pEl.addChild("span").setAttribute("class", "targetDesc").addText(": "
                + ((target.getDescription() == null) ? "" : target.getDescription()));

            Element tableEl = divEl.addChild("div").setAttribute("class", "indent").addChild("table");
            Element tdEl = addTargetRow(tableEl, "plugin");

            //            trEl.addText(": ");
            RepoArtifactId pluginId = target.getPlugin().getArtifact().getId();

            //            addId(tdEl, pluginId, false, false);
            addHelpLink(tdEl, repository.resolve(pluginId), false);

            if (target.getDependencies().size() != 0) {
                addTargetRow(tableEl, "depends").addText(Strings.join(target.getDependencies().iterator(), ","));
            }

            // Get all paths referenced
            Set paths = new TreeSet();

            for (Iterator j = target.getPathGroups().iterator(); j.hasNext();) {
                PathGroup group = (PathGroup)j.next();

                for (Iterator k = group.getPaths().iterator(); k.hasNext();) {
                    String path = (String)k.next();
                    paths.add(path);
                }
            }

            String pathsString = paths.toString();
            addTargetRow(tableEl, "paths").addText(pathsString.substring(1, pathsString.length() - 1));

            if (target.getPrefix() != null) {
                addTargetRow(tableEl, "properties").addText("Prefix: " + target.getPrefix());

                Element trEl = tableEl.addChild("tr");
                trEl.addChild("td");

                Element pTableEl = trEl.addChild("td").addChild("table");
                pTableEl.addChild("span");

                for (Iterator j = actualProperties.entrySet().iterator(); j.hasNext();) {
                    Map.Entry entry = (Map.Entry)j.next();
                    String key = (String)entry.getKey();

                    if (key.startsWith(target.getPrefix() + ".")) {
                        trEl = pTableEl.addChild("tr");
                        addTooltip(trEl.addChild("td").setAttribute("class", "targetAttribute").addText(key.substring(target.getPrefix()
                                    .length() + 1)), annotatedProperties.getAnnotation(key));
                        trEl.addChild("td").addText((String)entry.getValue());
                        j.remove();
                    }
                }
            }

            if (i.hasNext()) {
                divEl.addChild("br");
            }
        }
    }

    private Element addTargetRow(Element ulEl, String attribute) {
        Element trEl = ulEl.addChild("tr");
        trEl.addChild("td").setAttribute("align", "right").addChild("span").setAttribute("class", "targetAttribute")
        .addText(attribute);

        return trEl.addChild("td");
    }

    private void generatePluginPaths(ProjectModel model, Element divEl)
            throws IOException {
        // Display the paths
        for (Iterator i = getPluginPaths(model).entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            Plugin plugin = (Plugin)entry.getKey();
            Element pEl = divEl.addChild("p");
            addId(pEl, plugin.getArtifact().getId(), true, false);

            Set paths = (Set)entry.getValue();
            Element ulEl = divEl.addChild("ul");

            for (Iterator j = paths.iterator(); j.hasNext();) {
                String pathId = (String)j.next();
                pathId = pathId.substring(pathId.indexOf(".") + 1);
                addPath(ulEl, plugin.getArtifact().getPath(pathId), model.getPluginPath(plugin, pathId, true, false),
                    "li");
            }
        }
    }

    /**
     * Get all plugin paths used by the project, grouped by plugin
     */
    private Map getPluginPaths(ProjectModel model) {
        Map pathsByPlugin = new TreeMap(new Comparator() {
                    public int compare(Object o1, Object o2) {
                        Plugin plugin1 = (Plugin)o1;
                        Plugin plugin2 = (Plugin)o2;

                        return plugin1.getArtifact().getId().toShortString().compareTo(plugin2.getArtifact().getId()
                            .toShortString());
                    }
                });
        Map targets = model.getTargets();

        for (Iterator i = targets.values().iterator(); i.hasNext();) {
            Target target = (Target)i.next();
            Plugin plugin = target.getPlugin();
            Set paths = (Set)pathsByPlugin.get(plugin);

            if (paths == null) {
                paths = new TreeSet();
                pathsByPlugin.put(plugin, paths);
            }

            for (Iterator j = target.getPathGroups().iterator(); j.hasNext();) {
                PathGroup group = (PathGroup)j.next();

                for (Iterator k = group.getPaths().iterator(); k.hasNext();) {
                    String path = (String)k.next();

                    if (path.startsWith("plugin.")) {
                        paths.add(path);
                    }
                }
            }
        }

        return pathsByPlugin;
    }

    private void generateProjectPaths(ProjectModel model, Element divEl)
            throws IOException {
        SortedMap paths = new TreeMap(model.getProjectPaths());

        for (Iterator i = paths.values().iterator(); i.hasNext();) {
            Path path = (Path)i.next();
            List projectPath = model.getProjectPath(path.getId(), false, false);
            addPath(divEl, path, projectPath, "p");
        }
    }

    private void addPath(Element divEl, RepoPath path, List projectPath, String pathElementType)
            throws IOException {
        Element pEl = divEl.addChild(pathElementType);
        pEl = addTooltip(pEl, path.getAnnotations());
        pEl.addChild("span").setAttribute("class", "pathId").addText(path.getId());
        pEl.addChild("span").setAttribute("class", "pathDesc").addText(": " + path.getDescription());

        if (projectPath.size() == 0) { // Empty <ul/> don't render properly in quirks mode
            divEl.addChild("br");
        } else {
            Element el = divEl.addChild("div").setAttribute("class", "indent");
            el.addChild("span");
            toTree(el, projectPath);
        }
    }

    private List toTree(Element divEl, List projectPath)
            throws IOException {
        // Find top-level nodes (those declared by something outside the path itself
        List topLevel = new ArrayList();

        for (Iterator i = projectPath.iterator(); i.hasNext();) {
            RepoArtifact artifact = (RepoArtifact)i.next();

            if (!contains(projectPath, (RepoArtifactId)artifact.getId().getAnnotations().get("declaredBy"))) {
                topLevel.add(artifact);
            }
        }

        for (Iterator i = topLevel.iterator(); i.hasNext();) {
            RepoArtifact artifact = (RepoArtifact)i.next();
            addElements(divEl, artifact, projectPath, true);
        }

        divEl.addChild("br");

        return projectPath;
    }

    private void addElements(Element divEl, RepoArtifact artifact, List projectPath, boolean topLevel)
            throws IOException {
        RepoArtifactId id = artifact.getId();
        Element pEl = divEl.addChild("p").setAttribute("class", "artifactId");
        addId(addTooltip(pEl, id.getAnnotations()), id, false, false);

        Element nested = null;

        for (Iterator i = projectPath.iterator(); i.hasNext();) {
            RepoArtifact artifact1 = (RepoArtifact)i.next();

            if (artifact1.getId().getAnnotations().get("declaredBy") == artifact.getId()) {
                nested = (nested == null) ? divEl.addChild("div").setAttribute("class", "indent") : nested;
                nested.addChild("span");
                addElements(nested, artifact1, projectPath, false);
            }
        }
    }

    private Element addId(Element el, RepoArtifactId id, boolean versionOnly, boolean bold) {
        String colour = bold ? "black" : "grey";

        if (!versionOnly) {
            el.addChild("span").setAttribute("class", "artifactId " + colour).addText(id.getGroup() + " ");
            el.addChild("span").setAttribute("class", "artifactId-bold " + colour).addText(id.getName() + " ");
        }

        el.addChild("span").setAttribute("class", "artifactId " + colour).addText(id.getVersion().toString());

        return el;
    }

    private boolean contains(List projectPath, RepoArtifactId repoArtifactId) {
        for (Iterator i = projectPath.iterator(); i.hasNext();) {
            RepoArtifact artifact = (RepoArtifact)i.next();

            if (artifact.getId() == repoArtifactId) { // Compare by identity as we want the exact declaring instance

                return true;
            }
        }

        return false;
    }

    private Element addTooltip(Element element, Annotations annotations) {
        if (annotations == null) {
            return element;
        }

        Element spanEl = annotationsRoot.addChild("span").setAttribute("id", Integer.toString(++annotationId))
            .setAttribute("style", "display:none");
        Element tableEl = spanEl.addChild("table");

        for (Iterator i = annotations.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            Element trEl = tableEl.addChild("tr");
            trEl.addChild("td").addChild("b").addText((String)entry.getKey());
            formatValue(entry.getValue(), trEl.addChild("td"));
        }

        element.setAttribute("onmouseover", "TagToTip('" + annotationId + "')");

        return element;
    }

    private void formatValue(Object value, Element element) {
        if (value == null) {
            element.addText("");

            return;
        }

        if (value instanceof RepoArtifactId) {
            addId(element, (RepoArtifactId)value, false, false);
        } else {
            element.addText(value.toString());
        }
    }

    public void setMetadata(Metadata metadata) {
        this.metadata = metadata;
    }

    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    public void generateSummary() throws IOException {
        File inFile = getProperties().getFile("in");
        Document in = Document.parse(inFile, new Document.NullEntityResolver());

        File out = getProperties().getFile("out");
        destDir = getProperties().getFile("helpOut");

        Map transforms = getProperties().getMap("transforms", true, String.class);

        for (Iterator i = transforms.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();

            if ("archetypes".equals(entry.getKey())) {
                System.out.println("");
            }

            transform(in, inFile, (String)entry.getKey(), (String)entry.getValue());
        }

        save(out, in);
        logger().info("Summary saved to " + out.getAbsolutePath());
    }

    private void transform(Document in, File inFile, String divId, String pluginsString) {
        Element divEl = in.getRoot().getDescendent("div", "id", divId);
        Assert.isTrue(divEl != null, "Cannot find a div element with id=" + divId + " in " + inFile.getPath());

        List plugins = Strings.commaSepList(pluginsString);

        for (Iterator j = plugins.iterator(); j.hasNext();) {
            String plugin = (String)j.next();
            String[] tokens = Strings.split(plugin, ":");
            String group = tokens[0];
            String name = group.substring(group.lastIndexOf(".") + 1);
            String[] versions = Strings.split(tokens[1], ";");

            for (int k = 0; k < versions.length; k++) {
                boolean first = k == 0;
                boolean last = k == (versions.length - 1);
                String version = versions[k];
                RepoArtifactId id = new RepoArtifactId(group, name, "jar", version);
                RepoArtifact artifact = repository.resolve(id);
                HelpGenerator generator = null;

                if (first) {
                    generator = getGenerator(artifact, true);
                }

                generator.addSummary(divEl, first, last);
            }
        }
    }

    protected String getCatalogPath(RepoArtifactId id) {
        return "META-INF/quokka/" + id.toPathString() + "/catalog.xml";
    }

    private URL getURL(RepoArtifact artifact, String path) {
        return URLs.toURL(artifact.getLocalCopy(), path);
    }

    public HelpGenerator getGenerator(RepoArtifact artifact, boolean summary) {
        HelpGenerator generator;

        if (getURL(artifact, getPluginPath(artifact.getId())) != null) {
            generator = new PluginHelpGenerator();
        } else if (getURL(artifact, getCatalogPath(artifact.getId())) != null) {
            generator = new XmlCatalogHelpGenerator();
        } else if (getURL(artifact, getDependencySetPath(artifact.getId())) != null) {
            generator = new DepSetHelpGenerator();
        } else {
            generator = new DefaultHelpGenerator();
        }

        generator.setArtifact(artifact);
        generator.setPlugin(this);
        generator.setSummary(summary);
        generator.initialise();

        return generator;
    }

    protected String getDependencySetPath(RepoArtifactId id) {
        return "META-INF/quokka/" + id.toPathString() + "/depset.xml";
    }

    protected String getPluginPath(RepoArtifactId id) {
        return "META-INF/quokka/" + id.toPathString() + "/plugin.xml";
    }

    public void generateHelp() throws IOException {
        // Get the generator
        RepoArtifactId id = (RepoArtifactId)metadata.getArtifactIds("jar").iterator().next();
        RepoArtifact artifact = new RepoArtifact(id);
        artifact.setLocalCopy(getLifeCycle().getResourcesOutput());

        HelpGenerator generator = getGenerator(artifact, false);
        File out = new File(getLifeCycle().getResourcesOutput(), getHelpPath(id));
        out = getUtils().normalise(out.getAbsolutePath());

        if (generator.isUpToDate(out)) {
            logger().verbose("Skipping help generation as the generated help is up to date");

            return;
        }

        // Load template (which is modified to become the output)
        Document outDoc = Document.parse(getResources().getLocalResources().getAsStream("template.xhtml"),
                new Document.NullEntityResolver());

        generator.addContent(outDoc);

        save(out, outDoc);
        logger().info("Generated help to: " + out.getAbsolutePath());
    }

    protected String getHelpPath(RepoArtifactId id) {
        return "META-INF/quokka/" + id.toPathString() + "/help.html";
    }

    private void save(File file, Document document) throws IOException {
        getUtils().mkdir(file.getParentFile());

        FileWriter writer = new FileWriter(file);
        writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            + "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"
            + "        \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n");
        document.toXML(writer, true);
        logger().info("Generated report to: " + file.getAbsolutePath());
    }

    protected void addHelpLink(Element refsEl, RepoArtifact artifact, boolean versionOnly) {
        String path = getHelpPath(artifact.getId());
        final URL url = URLs.toURL(artifact.getLocalCopy(), path);

        if (url == null) {
            addId(refsEl, artifact.getId(), versionOnly, false);

            return; // No help to link to ...
        }

        final String helpPath = artifact.getId().toPathString() + ".html";
        addId(refsEl.addChild("a").setAttribute("href", helpPath), artifact.getId(), versionOnly, false);

        // Extract the help file
        new VoidExceptionHandler() {
                public void run() throws Exception {
                    InputStream in = url.openStream();

                    try {
                        //                    File helpOut = getProperties().getFile("helpOut");
                        File helpOut = getUtils().normalise(destDir + "/" + helpPath);
                        getUtils().mkdir(helpOut.getParentFile());

                        OutputStream out = new BufferedOutputStream(new FileOutputStream(helpOut));

                        try {
                            new IOUtils().copyStream(in, out);
                        } finally {
                            out.close();
                        }
                    } finally {
                        in.close();
                    }
                }
            };
    }

    //~ Inner Classes --------------------------------------------------------------------------------------------------

    public static class PathNode {
        RepoArtifactId id;
        List children = new ArrayList();

        public PathNode(RepoArtifactId id) {
            this.id = id;
        }

        public RepoArtifactId getId() {
            return id;
        }

        public List getChildren() {
            return children;
        }
    }
}
